(ns util.exec
  (:use [clojure.java.io :only [file]]
        clojure.tools.logging
        util.io)
  (:require clojure.string))

;;; you may want to use clojure.java.shell

;;; typical usages:
;;; by default, start a process and wait until it finishes. inherit stdout and stderr
;;; if you want exec returns immediately, use :spawn
;;; if you don't want the subprocess killed after parent process exits,
;;;   a) use non-dummy stdout/stderr (the side effect is a thread is started, and prevents the JVM from exiting)
;;;   b) wait returned process explicitly, or
;;;   c) use start ... in windows
;;; Note: non-dummy stdout/stderr will start a thread, which may prevent the JVM from exiting

(defn- bytes-input-stream-handler [bytes]
  #(doto %
     (.write bytes)
     (.flush)
     (.close)))

(defn- input-stream-handler [in]
  (cond
    (string? in) (bytes-input-stream-handler (.getBytes in "UTF-8"))
    (= (class in) (Class/forName "[B")) (bytes-input-stream-handler in)
    :else (throw (Exception. "the input type is not supported"))))

(defn do-line-for-stream [do-line input]
  (with-open [input (java.io.BufferedReader. (java.io.InputStreamReader. input))]
    (loop []
      (let [line (.readLine input)]
        (when line
          (do-line line)
          (Thread/sleep 5) ; XXX: I don't know why sleeping 5 here, but I notice org.apache.tools.ant.taskdefs.Exec does this so...
          (recur))))))

(defn liner [do-line]
  #(do-line-for-stream do-line %))

(defn print-stdout [line]
  (log "STDOUT" :info nil line))

(defn print-stderr [line]
  (log "STDERR" :error nil line))

(defn- output-stream-handler [out line default-line]
  (case out
    :dummy (liner (fn [&args]))
    :external nil
    (or out (liner (or line default-line)))))

(def valid-exec-keys #{:env :dir :stdin :stdout :stdout-line :stderr :stderr-line :spawn :daemon :search})

(defn- check-invalid-keys [opts valid-keys]
  (seq (apply disj (set (keys opts)) valid-keys)))

(defn exec [argv & opts]
  "
  :env => a map for enviroment variables
  :dir => working directory
  :stdout => accept a function, which accept a stdout stream of subprocess. If :dummy is specified, all stdout output will be ignored.
  :stdout-line => a function, wihch will be called each time a new line is outputed from subprocess's standard output
  :stderr => similar to :stdout, but working on standard error
  :stderr-line => similar to :stdout-line, but working on standard error
  :spwan => no blocking. Default false, which means the exec will never returns until the the subprocess is killed
  :daemon => when set to true, JVM process won't wait child process. Default: false.
  "
  (let [opts (apply array-map opts)
        invalid-options (check-invalid-keys opts valid-exec-keys)
        builder (ProcessBuilder. argv)]
    (when invalid-options
      (throw (Exception. (str "Invalid exec parameters: " (clojure.string/join ", " invalid-options)))))
    (debug (str "Executing " argv))
    (when (:env opts)
      (let [env (:env opts)
            oldenv (.environment builder)]
        (dorun (map (fn [[k v]] (if v (.put oldenv k v) (.remove oldenv k))) env))))
    (when (:dir opts)
      (.directory builder (file (:dir opts))))
    (when (:search opts) (throw (Exception. ":search is not supported yet!")))
    (let [process (.start builder)
          stdin-thread (when (:stdin opts)
                         (let [stdin-stream (.getOutputStream process)
                               stdin-handler (input-stream-handler (:stdin opts))
                               stdin-thread (Thread. #(stdin-handler stdin-stream))]
                           (when (:daemon opts)
                             (.setDaemon stdin-thread true))
                           (.start stdin-thread)
                           stdin-thread))
          stdout-thread (when-not (= (:stdout opts) :external)
                          (let [stdout-stream (.getInputStream process)
                                stdout-handler (output-stream-handler (:stdout opts) (:stdout-line opts) print-stdout)
                                stdout-thread (Thread. #(stdout-handler stdout-stream))]
                            (when (:daemon opts)
                              (.setDaemon stdout-thread true))
                            (.start stdout-thread)
                            stdout-thread))
          stderr-thread (when-not (= (:stderr opts) :external)
                          (let [stderr-stream (.getErrorStream process)
                                stderr-handler (output-stream-handler (:stderr opts) (:stderr-line opts) print-stderr)
                                stderr-thread (Thread. #(stderr-handler stderr-stream))]
                            (when (:daemon opts)
                              (.setDaemon stderr-thread true))
                            (.start stderr-thread)
                            stderr-thread))]
      (trace (str "Created process " argv))
      (if (:spawn opts)
        process
        (do
          (debug "Waiting for process termination")
          (.waitFor process)
          (when stdin-thread (.join stdin-thread))
          (when stdout-thread (.join stdout-thread))
          (when stderr-thread (.join stderr-thread))
          (.destroy process)
          (debug (str "Process exited " argv))
          (.exitValue process))))))

(def valid-popen-keys #{:env :dir :search})

;; TODO: collecting stderr with stdout
(defn popen [argv & opts]
  (let [opts (apply array-map opts)
        invalid-options (check-invalid-keys opts valid-popen-keys)]
    (when invalid-options
      (throw (Exception. (str "Invalid popen parameters: " (clojure.string/join ", " invalid-options)))))
    (let [process (apply exec argv (apply concat (assoc opts :spawn true :stdout :external :stderr :external)))
            stdout-stream (.getInputStream process)]
      (with-open [stdout-stream (.getInputStream process)]
        (let [output (String. (stream->bytes stdout-stream))]
          (.waitFor process)
          (.destroy process)
          output)))))
            

;; TODO: exec-batch
